using System;
using FPLibrary;
using UnityEngine;

namespace PathX.Unity
{
    public class GridVisualizer : Visualizer
    {
        /// <summary>
        /// The model attributes
        /// </summary>
        public int modelAttributes;

        /// <summary>
        /// Whether to draw the grids sub sections
        /// </summary>
        [Tooltip("Draw a crude representation of the grid's sub sections.")]
        public bool drawSubSections;

        /// <summary>
        /// Controls whether the visualizer draws all grids in the scene or only those on the same GameObject as the visualizer.
        /// </summary>
        [Tooltip("Draw all grids in the scene, i.e. only one visualizer needed.")]
        public bool drawAllGrids = false;

        /// <summary>
        /// The editor refresh delay, i.e. increase this value if you experience too much CPU activity during scene modification
        /// </summary>
        [Tooltip("The delay between updates after changes have been made to the scene.")]
        public int editorRefreshDelay = 100;

        /// <summary>
        /// The distance threshold controlling when the grid is drawn. If the camera is showing more than this looking at the diagonal, the grid will no longer be drawn.
        /// This is for performance reasons plus when zoomed out too far the grid visualization is useless.
        /// </summary>
        [Tooltip("How zoomed out can you be and still see the grid visuals.")]
        public float drawDistanceThreshold = 175f;

        /// <summary>
        /// The grid lines color
        /// </summary>
        [Tooltip("The color of the grid lines.")]
        public Color gridLinesColor = new Color(135f / 255f, 135f / 255f, 135f / 255f);

        /// <summary>
        /// The color of accessibility lines where only descent is possible
        /// </summary>
        [Tooltip("The color of accessibility lines where only descent is possible.")]
        public Color descentOnlyColor = new Color(175f / 255f, 6175 / 255f, 60 / 255f);

        /// <summary>
        /// The color of accessibility lines where only ascent is possible
        /// </summary>
        [Tooltip("The color of accessibility lines where only ascent is possible.")]
        public Color ascentOnlyColor = new Color(60 / 255f, 60f / 255f, 175f / 255f);

        /// <summary>
        /// The obstacle color
        /// </summary>
        [Tooltip("The color of obstructed cells.")]
        public Color obstacleColor = new Color(226f / 255f, 41f / 255f, 32f / 255f, 150f / 255f);

        /// <summary>
        /// The sub sections color
        /// </summary>
        [Tooltip("The color of the sub sections visualization.")]
        public Color subSectionsColor = new Color(0f, 150f / 255f, 211f / 255f);

        /// <summary>
        /// The color of the bounds wire frame
        /// </summary>
        [Tooltip("The color of the bounds frame.")]
        public Color boundsColor = Color.grey;

        private DateTime? _nextRefresh;


        /// <summary>
        /// Draws the actual visualization.
        /// </summary>
        protected override void DrawVisualization()
        {
            var forceRefresh = true;
            // if (_nextRefresh.HasValue && _nextRefresh < DateTime.UtcNow)
            // {
            //     _nextRefresh = null;
            //     forceRefresh = true;
            // }

            var grids = this.drawAllGrids ? FindObjectsOfType<PathGridComponent>() : GetComponents<PathGridComponent>();

            if (grids != null)
            {
                foreach (var grid in grids)
                {
                    if (grid.enabled)
                    {
                        bool outlineOnly;
                        var drawArea = GetDrawArea(grid.origin.y, out outlineOnly);
                        if (!drawArea.Overlaps(grid.bounds))
                        {
                            continue;
                        }

                        grid.EnsureForEditor(forceRefresh, drawArea);
                        DrawGrid(grid, drawArea, outlineOnly);
                    }
                }
            }
        }

        private Bounds GetDrawArea(float gridElevation, out bool outlineOnly)
        {
            outlineOnly = false;

            //Determine the area visible through the camera, starting with the bottom left and top right corners.
            var cam = Camera.current;
            var bl = cam.ScreenToGroundPoint(Vector3.zero, gridElevation);
            var tr = cam.ScreenToGroundPoint(new Vector3(cam.pixelWidth, cam.pixelHeight, 0f), gridElevation);
            if (bl == Vector3.zero || tr == Vector3.zero)
            {
                outlineOnly = true;
            }

            //We do not want to draw the grids if zoom level is too high since it will simply crash the editor to draw that many lines.
            var diagDistSquared = (tr - bl).sqrMagnitude;
            if (diagDistSquared > this.drawDistanceThreshold * this.drawDistanceThreshold)
            {
                outlineOnly = true;
            }

            //Get the remaining corners resolved and calculate the proper bounds to draw
            var tl = cam.ScreenToGroundPoint(new Vector3(0f, cam.pixelHeight, 0f), gridElevation);
            var br = cam.ScreenToGroundPoint(new Vector3(cam.pixelWidth, 0f, 0f), gridElevation);

            var wpMin = new Vector3(Mathf.Min(bl.x, tl.x, br.x, tr.x), 0f, Mathf.Min(bl.z, tl.z, br.z, tr.z));
            var wpMax = new Vector3(Mathf.Max(bl.x, tl.x, br.x, tr.x), 0f, Mathf.Max(bl.z, tl.z, br.z, tr.z));
            var drawArea = new Bounds();
            drawArea.SetMinMax(wpMin, wpMax);

            return drawArea;
        }

        private bool DrawGrid(PathGridComponent pathGrid, Bounds drawArea, bool outlineOnly)
        {
            if (pathGrid.sizeX == 0 || pathGrid.sizeZ == 0 || pathGrid.cellSize == 0f)
            {
                return true;
            }

            Gizmos.color = this.boundsColor;
            Gizmos.DrawWireCube(pathGrid.bounds.center, pathGrid.bounds.size);

            IGrid grid = pathGrid.editorGrid;
            if (outlineOnly || grid == null)
            {
                return true;
            }

            var start = grid.GetCell(FPVector.ToFPVector(drawArea.min));
            var end = grid.GetCell(FPVector.ToFPVector(drawArea.max));
            if (start == null || end == null)
            {
                return false;
            }

            var lineColor = this.gridLinesColor;

            DrawLayout(grid, start, end, lineColor);

            return true;
        }

        private bool DrawLayout(IGrid grid, Cell start, Cell end, Color lineColor)
        {
            Gizmos.color = lineColor;

            var step = grid.cellSize;
            var halfCell = (step / 2.0f);
            var y = grid.origin.y + 0.05f;

            var xMin = start.position.x - halfCell;
            var xMax = end.position.x + halfCell;
            var zMin = start.position.z - halfCell;
            var zMax = end.position.z + halfCell;

            for (var x = xMin; x <= xMax; x += step)
            {
                var pos1 = new FPVector(x, y, zMin).ToVector();
                var pos2 = new FPVector(x, y, zMax).ToVector();
                Gizmos.DrawLine(pos1, pos2);
            }

            for (var z = zMin; z <= zMax; z += step)
            {
                var pos1 = new FPVector(xMin, y, z).ToVector();
                var pos2 = new FPVector(xMax, y, z).ToVector();
                Gizmos.DrawLine(pos1, pos2);
            }
            
            var matrix = grid.cellMatrix;
            for (int x = start.matrixPosX; x <= end.matrixPosX; x++)
            {
                for (int z = start.matrixPosZ; z <= end.matrixPosZ; z++)
                {
                    var c = matrix[x, z];
                    if (c == null)
                    {
                        return false;
                    }

                    var walkableToSomeone = c.IsWalkable(AttributeMask.All);
                    var walkableToEveryone = c.IsWalkable(AttributeMask.None);

                    if (!walkableToSomeone)
                    {
                        Gizmos.color = this.obstacleColor;
                        Gizmos.DrawCube(c.position.ToVector(), new FPVector(step, 0.05f, step).ToVector());
                    }
                    else if (!walkableToEveryone)
                    {
                        var half = this.obstacleColor;
                        half.a = half.a * 0.5f;
                        Gizmos.color = half;
                        Gizmos.DrawCube(c.position.ToVector(), new FPVector(step, 0.05f, step).ToVector());
                    }
                }
            }

            return true;
        }

        private void Update()
        {
            if (Application.isEditor && !Application.isPlaying)
            {
                _nextRefresh = DateTime.UtcNow.AddMilliseconds(this.editorRefreshDelay);
            }
        }
    }

    public static class UnityExtension
    {
        public static Vector3 ScreenToGroundPoint(
            this Camera cam,
            Vector3 screenPos,
            float groundHeight)
        {
            Ray ray = cam.ScreenPointToRay(screenPos);
            float enter;
            return new Plane(Vector3.up, new Vector3(0.0f, groundHeight, 0.0f)).Raycast(ray, out enter)
                ? ray.GetPoint(enter)
                : Vector3.zero;
        }
    }
}